home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
PC Media 20
/
PC MEDIA CD20.iso
/
share
/
prog
/
cursoasm
/
cap5.msg
< prev
next >
Wrap
Text File
|
1993-06-26
|
15KB
|
274 lines
INTRODUCCION AL ASM: LA PILA
============================
Como comentábamos al final del cuarto capítulo, en este quinto capítulo
vamos a estudiar uno de los elementos de mayor utilidad del ASM: la pila.
Al estudiarla veremos dos nuevos registros, el SS y el SP, y dos nuevas
instrucciones, PUSH y POP.
Primero explicaré lo que se entiende por una pila en general y después vere-
mos la pila del 8086 en concreto. Por eso, si alguien sabe lo que es una pila,
puede saltarse algunas líneas.
Un ordenador siempre guarda los datos en la memoria, exceptuando los regis-
tros de la CPU. Estos datos pueden estar dispuestos de diferentes maneras, y se
puede acceder a ellos de diversas formas. Una particular organización y modo de
acceso a los datos se suele denominar 'estructura de datos', de las que existen
muchas y muy diferentes, cada una adecuada para un propósito. Son estructuras
de datos los arrays, las listas encadenadas, los árboles binarios, etc...
Las estructuras que nos interesan ahora aquí son las estructuras lineales,
en que cada elemento (cada dato) va después del anterior. Es decir, cada ele-
mento tiene un predecesor y un sucesor, excepto el primero y el último. Son
estructuras de este tipo los arrays y las listas encadenadas, mientras que no
lo son los árboles binarios (no os preocupéis los que no sepáis que son estas
cosillas). Las estructuras lineales se pueden clasificar principalmente en dos
grupos: las llamadas FIFO (First In First Out, el primero en entrar es el pri-
mero en salir) y las llamadas LIFO (Last In First Out, el último en entrar es
el primero en salir). Aquí van dos ejemplos de cada una para que quede claro:
Una estructura FIFO es similar a la cola del autobús: los elementos se orde-
nan según llegan (los pasajeros se ponen a la cola cada uno detrás del anterior
en llegar) y se sacan empezando por el primero que llegó y acabando por el úl-
timo (el primero en llegar a la parada es el primero en montar). Esta estructu-
ra se suele denominar 'cola' o 'queue' ('cola' en inglés).
En cambio, una estructura LIFO es similar al montón de papeles que suelen
estar pinchados todos juntos en una tintorería: los elementos se van ordenando
según llegan (cada papel se pincha sobre el anterior), pero al sacarse del
montón se sacan empezando por el último que llegó (se empiezan a sacar por el
último que se pinchó, justo en orden inverso al que se pincharon). Esta estruc-
tura se suele denominar 'pila' o 'stack' ('pila' en inglés).
Estas estructuras se pueden implementar de muchas maneras diferentes por un
programa (todavía no estamos viendo la pila del 8086 en concreto, ésta viene ya
implementada por el hard del uP). Una manera sencilla de hacerlo es un array
del tipo de dato que queremos cada elemento: si queremos una cola de enteros
será un array de enteros, si queremos una pila de números reales será un array
de números en coma flotante, etc... Necesitaremos también una variable entera
que llevará la cuenta de los elementos insertados hasta el momento, que llama-
remos 'cuenta'. Veamos cómo se implementaría una pila con este esquema:
inicialmente, la variable 'cuenta' valdría -1 (suponemos que los índices de los
arrays van como en C, comenzando por el cero). Para insertar un elemento, se
incrementaría 'cuenta' y se guardaría el elemento en la posición número 'cuen-
ta' del array. De esta manera, siempre tendríamos accesible el último valor
empujado en 'pila[cuenta]'. Una jugada inteligente sería meter en este punto
una comprobación de que 'cuenta' no ha sobrepasado el límite superior de la
pila. En caso de que así fuera, emitiríamos un mensaje de error. Este error se
suele denominar 'stack overflow' o 'desbordamiento de la pila', y aunque en la
pila del 8086 no se comprueba internamente, los compiladores de lenguajes de
alto nivel suelen incluir código para comprobarlo. A esta operación de intro-
ducir un valor en la pila se le suele llamar 'empujar' un valor en la pila (en
inglés 'push').
La otra operación que nos queda ver es la operación contraria, la de sacar
un valor de la pila. Ya que queremos acceder a la estructura en la forma LIFO,
cada vez que se extrae un valor debe ser del extremo superior de la pila. Por
ejemplo, si 'cuenta' vale 4 y queremos extraer un valor, éste deberá tomarse de
la posición 4 del array, que será el último elemento empujado. Además, hay que
decrementar la variable 'cuenta' para apuntar al nuevo 'tope' de la pila. Es
importante tener en cuenta que no hace falta sobreescribir la posición 4 del
array, esto ocurrirá la próxima vez que se empuje un valor, sino que basta con
decrementar la variable 'cuenta'. A esta otra operación se le llama 'pop' en
inglés, en español se suele decir simplemente 'sacar' o 'extraer' un valor de
la pila.
La pila del 8086 viene implementada por el hard del uP, pero los datos resi-
den en memoria. El uP tiene dos registros para manejar la pila: el SS ('Stack
Segment', 'segmento de pila'), que es el cuarto registro de segmento que junto
con CS, DS y ES se utilizan para formar direcciones de 20 bits, y el SP ('Stack
Pointer', 'puntero de pila'), que es un registro de 16 bits que cumple la
función de nuestra variable 'cuenta' y que se une al SP para formar la direc-
ción de memoria completa. Cada programa suele destinar una zona de la memoria
para la pila, y es esta zona la que alberga los valores introducidos en ésta.
Todos los elementos de la pila son valores enteros de 16 bits, por lo que se
utiliza una palabra de la memoria para cada valor.
Un aspecto que en un principio puede llamar la atención es que la pila del
8086, al igual que la de todos los micros que conozco, crece hacia abajo en
lugar de hacia arriba. El funcionamiento de ésta es, por lo demás, idéntico al
de nuestra pila imaginaria: al principio 'cuenta' apuntaría al último elemento
del array más uno, al empujar un valor se se decrementaría cuenta y metería en
'pila[cuenta]' , y al sacarlo se incrementaría cuenta y se devolvería
'pila[cuenta]'. Más adelante se verán las ventajas de que la pila crezca hacia
abajo.
Los registros SS y SP forman la dirección absoluta donde reside el último
valor empujado. Al empujar un valor, se resta dos a SP (porque cada elemento
son dos bytes, es decir, dos posiciones de memoria) y el nuevo valor se guarda
en la dir SS:SP (y el byte de mayor peso en SS:SP+1). Al sacar un valor, se
toma de las posiciones SS:SP y SS:SP+1 y se suma dos a SP.
Las instrucciones PUSH y POP nos permiten empujar un valor de 16 bits y
recoger el último valor empujado. El uP se encarga de actualizar SP.
Vamos a ver un ejemplo un poco 'incivilizado' de cómo pondríamos a punto la
pila en una dirección determinada. Digo 'incivilizado' porque lo hacemos en una
dirección arbitraria, donde en un PC podría haber un driver o cualquier cosa.
De esto normalmente se encarga la rutina del DOS que carga un programa y lo
ejecuta, por lo que sólo es un ejemplo para comprender el funcionamiento.
Como sagazmente habréis deducido (y por si acaso no, os lo cuento), ya que
la gestión de la pila se hace modificando sólo el SP al hacer PUSH y POP, la
pila no puede exceder de 64K. Supongamos que queremos poner una pila de 64K en
el segmento que comienza en la dirección absoluta 80000h (8000h:0). La última
palabra de este segmento está en 8FFFEh (el byte de menor peso en ésta y el de
mayor en la 8FFFFh). Por tanto, habría que cargar los registros como sigue:
MOV AX,8000h
MOV SS,AX
MOV SP,0FFFEh
Más adelante veremos que esto no se puede hacer así, sino que hay que tener
en cuenta las interrupciones, etc.. Por lo que no lo probéis. Pero para hacerse
una idea, está bien. En realidad, no es necesario reservar 64K para la pila,
por lo que SP no tiene por qué inicializarse a 0FFFEh. Por ejemplo, si queremos
reservar 32K para la pila inicializaremos SP a 7FFEh.
El funcionamiento exacto de la pila es el siguiente: cuando se ejecuta una
instrucción PUSH se decrementa SP en dos unidades y se guarda en la dirección
SS:SP el valor a empujar. De esta forma, SS:SP siempre apunta al último valor
empujado. Cuando se ejecuta un POP, se recoge el valor de la dirección SS:SP y
se incrementa SP en dos unidades.
Ambas instrucciones llevan un solo operando, que especifica de donde se toma
el valor (en el caso de PUSH) o donde se almacena (en el caso de POP). Los
operandos pueden ser uno cualquiera de los siguientes:
- Un registro de 16 bits de los siguientes: AX, BX, CX, DX, SI, DI, BP o SP.
- Un registro de segmento (no se permite POPear CS, porque implicaría un
salto del programa a otra dirección, y para esto ya hay otras instruccio-
nes).
- Una referencia a memoria (con cualquiera de los modos de direccionamiento
que vimos con la instrucción MOV).
- Un valor inmediato (por ejemplo, PUSH 55AAh) (sólo para PUSH y en 386 o
superiores).
Veamos un pequeño listado en ASM y los contenidos de la pila y los registros
con cada instrucción. El listado es el siguiente:
MOV AX,8000h
MOV SS,AX
MOV SP,0FFFEh ; inicializa la pila
MOV AX,55AAh
PUSH AX ; empuja el valor 55AAh
MOV BX,AX
ADD BX,1111h ; suma a BX el valor 1111h
PUSH BX
POP AX
POP BX
Estudiemos lo que ocurre a cada paso:
* MOV AX,8000h
* MOV SS,AX
* MOV SP,0FFFEh ; inicializa la pila
Estas instrucciones inicializan SS y SP para situar la pila al final del
segmento que comienza en la dirección absoluta 80000h. En este caso, no nos
interesan los contenidos anteriores de esa zona de memoria. Representaremos el
contenido de la pila como sigue:
Dirección de memoria Valor Registros del uP
-------------------------------------- ------------ ------------------
Segmento Offset Dirección absoluta Valor SS = 8000h
-------- ------ ------------------ ----- SP = 0FFFEh
8000h 0FFFFh 8FFFFh .............. ??
8000h 0FFFEh 8FFFEh .............. ??
8000h 0FFFDh 8FFFDh .............. ??
8000h 0FFFCh 8FFFCh .............. ??
8000h 0FFFBh 8FFFBh .............. ??
8000h 0FFFAh 8FFFAh .............. ??
* MOV AX,55AAh
Después de esta instrucción, el registro AX contendrá 55AAh.
* PUSH AX ; empuja el valor 55AAh
Nada más ejecutarla, así quedarán la pila y los registros del micro:
Dirección de memoria Valor Registros del uP
-------------------------------------- ------------ ------------------
Segmento Offset Dirección absoluta Valor SS = 8000h
-------- ------ ------------------ ----- SP = 0FFFCh
8000h 0FFFFh 8FFFFh .............. ?? AX = 55AAh
8000h 0FFFEh 8FFFEh .............. ??
8000h 0FFFDh 8FFFDh .............. 55h
8000h 0FFFCh 8FFFCh .............. AAh
8000h 0FFFBh 8FFFBh .............. ??
8000h 0FFFAh 8FFFAh .............. ??
* MOV BX,AX
* ADD BX,1111h ; suma a BX el valor 1111h
* PUSH BX
Ya que 55AAh + 1111h = 66BBh, así quedarán las cosas:
Dirección de memoria Valor Registros del uP
-------------------------------------- ------------ ------------------
Segmento Offset Dirección absoluta Valor SS = 8000h
-------- ------ ------------------ ----- SP = 0FFFAh
8000h 0FFFFh 8FFFFh .............. ?? AX = 55AAh
8000h 0FFFEh 8FFFEh .............. ?? BX = 66BBh
8000h 0FFFDh 8FFFDh .............. 55h
8000h 0FFFCh 8FFFCh .............. AAh
8000h 0FFFBh 8FFFBh .............. 66h
8000h 0FFFAh 8FFFAh .............. BBh
* POP AX
Se tomará el valor de SS:SP, se guardará en AX, y se sumará dos a SP:
Dirección de memoria Valor Registros del uP
-------------------------------------- ------------ ------------------
Segmento Offset Dirección absoluta Valor SS = 8000h
-------- ------ ------------------ ----- SP = 0FFFCh
8000h 0FFFFh 8FFFFh .............. ?? AX = 66BBh
8000h 0FFFEh 8FFFEh .............. ?? BX = 66BBh
8000h 0FFFDh 8FFFDh .............. 55h
8000h 0FFFCh 8FFFCh .............. AAh
8000h 0FFFBh 8FFFBh .............. 66h
8000h 0FFFAh 8FFFAh .............. BBh
* POP BX
Análogo a la instrucción anterior, pero con BX:
Dirección de memoria Valor Registros del uP
-------------------------------------- ------------ ------------------
Segmento Offset Dirección absoluta Valor SS = 8000h
-------- ------ ------------------ ----- SP = 0FFFEh
8000h 0FFFFh 8FFFFh .............. ?? AX = 66BBh
8000h 0FFFEh 8FFFEh .............. ?? BX = 55AAh
8000h 0FFFDh 8FFFDh .............. 55h
8000h 0FFFCh 8FFFCh .............. AAh
8000h 0FFFBh 8FFFBh .............. 66h
8000h 0FFFAh 8FFFAh .............. BBh
Os habréis dado cuenta de que podríamos haber inicializado SP a 0 para apro-
vechar también los dos últimos bytes del segmento. En realidad, esto es algo
de lo que no es necesario preocuparse prácticamente nunca, pues el DOS se en-
carga de inicializar la pila generalmente.
Uno de los usos más habituales de la pila es el de preservar el contenido de
uo o varios registros que van a ser utilizados en un fragmento de código. Suele
ser algo así:
PUSH AX ; Guardamos AX, BX y CX
PUSH BX
PUSH CX
; [...] Código que modifica AX, BX y CX
POP CX ; Recuperamos AX, BX y CX
POP BX
POP CX
Ahora ya estamos preparados para introducir un importante aspecto de la
programación en ASM: las interrupciones. Conociendo las interrupciones, ya ten-
dremos el bagaje necesario para acometer la escritura de programas usando
ensambladores comerciales como el TASM o el MASM, comenzando con el ya tradi-
cional 'Hello, world'.
Salut :-)
Jon